{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b7904228",
   "metadata": {},
   "source": [
    "# 앙상블 검색기(Ensemble Retriever)\n",
    "\n",
    "`EnsembleRetriever`는 여러 검색기를 결합하여 더 강력한 검색 결과를 제공하는 LangChain의 기능입니다. 이 검색기는 다양한 검색 알고리즘의 장점을 활용하여 단일 알고리즘보다 더 나은 성능을 달성할 수 있습니다.\n",
    "\n",
    "**주요 특징**\n",
    "1. 여러 검색기 통합: 다양한 유형의 검색기를 입력으로 받아 결과를 결합합니다.\n",
    "2. 결과 재순위화: [Reciprocal Rank Fusion](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) 알고리즘을 사용하여 결과의 순위를 조정합니다.\n",
    "3. 하이브리드 검색: 주로 `sparse retriever`(예: BM25)와 `dense retriever`(예: 임베딩 유사도)를 결합하여 사용합니다.\n",
    "\n",
    "**장점**\n",
    "- Sparse retriever: 키워드 기반 검색에 효과적\n",
    "- Dense retriever: 의미적 유사성 기반 검색에 효과적\n",
    "\n",
    "이러한 상호 보완적인 특성으로 인해 `EnsembleRetriever`는 다양한 검색 시나리오에서 향상된 성능을 제공할 수 있습니다.\n",
    "\n",
    "자세한 내용은 [LangChain 공식 문서](https://python.langchain.com/docs/modules/data_connection/retrievers/ensemble)를 참조하세요.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19224b47",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "75d27ac8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH10-Retriever\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45ff8adf",
   "metadata": {},
   "source": [
    "- `EnsembleRetriever`를 초기화하여 `BM25Retriever`와 `FAISS` 검색기를 결합합니다. 각 검색기의 가중치를 설정됩니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3f2b299c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.retrievers import BM25Retriever, EnsembleRetriever\n",
    "from langchain.vectorstores import FAISS\n",
    "from langchain_openai import OpenAIEmbeddings\n",
    "\n",
    "# 샘플 문서 리스트\n",
    "doc_list = [\n",
    "    \"I like apples\",\n",
    "    \"I like apple company\",\n",
    "    \"I like apple's iphone\",\n",
    "    \"Apple is my favorite company\",\n",
    "    \"I like apple's ipad\",\n",
    "    \"I like apple's macbook\",\n",
    "]\n",
    "\n",
    "\n",
    "# bm25 retriever와 faiss retriever를 초기화합니다.\n",
    "bm25_retriever = BM25Retriever.from_texts(\n",
    "    doc_list,\n",
    ")\n",
    "bm25_retriever.k = 1  # BM25Retriever의 검색 결과 개수를 1로 설정합니다.\n",
    "\n",
    "embedding = OpenAIEmbeddings()  # OpenAI 임베딩을 사용합니다.\n",
    "faiss_vectorstore = FAISS.from_texts(\n",
    "    doc_list,\n",
    "    embedding,\n",
    ")\n",
    "faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={\"k\": 1})\n",
    "\n",
    "# 앙상블 retriever를 초기화합니다.\n",
    "ensemble_retriever = EnsembleRetriever(\n",
    "    retrievers=[bm25_retriever, faiss_retriever],\n",
    "    weights=[0.7, 0.3],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "835eb07f",
   "metadata": {},
   "source": [
    "`ensemble_retriever` 객체의 `get_relevant_documents()` 메서드를 호출하여 관련성 높은 문서를 검색합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cfacdd8d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 검색 결과 문서를 가져옵니다.\n",
    "query = \"my favorite fruit is apple\"\n",
    "ensemble_result = ensemble_retriever.invoke(query)\n",
    "bm25_result = bm25_retriever.invoke(query)\n",
    "faiss_result = faiss_retriever.invoke(query)\n",
    "\n",
    "# 가져온 문서를 출력합니다.\n",
    "print(\"[Ensemble Retriever]\")\n",
    "for doc in ensemble_result:\n",
    "    print(f\"Content: {doc.page_content}\")\n",
    "    print()\n",
    "\n",
    "print(\"[BM25 Retriever]\")\n",
    "for doc in bm25_result:\n",
    "    print(f\"Content: {doc.page_content}\")\n",
    "    print()\n",
    "\n",
    "print(\"[FAISS Retriever]\")\n",
    "for doc in faiss_result:\n",
    "    print(f\"Content: {doc.page_content}\")\n",
    "    print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dac4523b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 검색 결과 문서를 가져옵니다.\n",
    "query = \"Apple company makes my favorite iphone\"\n",
    "ensemble_result = ensemble_retriever.invoke(query)\n",
    "bm25_result = bm25_retriever.invoke(query)\n",
    "faiss_result = faiss_retriever.invoke(query)\n",
    "\n",
    "# 가져온 문서를 출력합니다.\n",
    "print(\"[Ensemble Retriever]\")\n",
    "for doc in ensemble_result:\n",
    "    print(f\"Content: {doc.page_content}\")\n",
    "    print()\n",
    "\n",
    "print(\"[BM25 Retriever]\")\n",
    "for doc in bm25_result:\n",
    "    print(f\"Content: {doc.page_content}\")\n",
    "    print()\n",
    "\n",
    "print(\"[FAISS Retriever]\")\n",
    "for doc in faiss_result:\n",
    "    print(f\"Content: {doc.page_content}\")\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9d4a5a2",
   "metadata": {},
   "source": [
    "## 런타임 Config 변경\n",
    "\n",
    "런타임에서도 retriever 의 속성을 변경할 수 있습니다. 이는 `ConfigurableField` 클래스를 사용하여 가능합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aae1c63e",
   "metadata": {},
   "source": [
    "- `weights` 매개변수를 `ConfigurableField` 객체로 정의합니다.\n",
    "  - 필드의 ID는 \"ensemble_weights\"로 설정합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84a34e4b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import ConfigurableField\n",
    "\n",
    "\n",
    "ensemble_retriever = EnsembleRetriever(\n",
    "    # 리트리버 목록을 설정합니다. 여기서는 bm25_retriever와 faiss_retriever를 사용합니다.\n",
    "    retrievers=[bm25_retriever, faiss_retriever],\n",
    ").configurable_fields(\n",
    "    weights=ConfigurableField(\n",
    "        # 검색 매개변수의 고유 식별자를 설정합니다.\n",
    "        id=\"ensemble_weights\",\n",
    "        # 검색 매개변수의 이름을 설정합니다.\n",
    "        name=\"Ensemble Weights\",\n",
    "        # 검색 매개변수에 대한 설명을 작성합니다.\n",
    "        description=\"Ensemble Weights\",\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb0e4fb1",
   "metadata": {},
   "source": [
    "- 검색 시 `config` 매개변수를 통해 검색 설정을 지정합니다.\n",
    "  - `ensemble_weights` 옵션의 가중치를 [1, 0]으로 설정하여 **모든 검색 결과의 가중치가 BM25 retriever 에 더 많이 부여** 되도록 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1bee1000",
   "metadata": {},
   "outputs": [],
   "source": [
    "config = {\"configurable\": {\"ensemble_weights\": [1, 0]}}\n",
    "\n",
    "# config 매개변수를 사용하여 검색 설정을 지정합니다.\n",
    "docs = ensemble_retriever.invoke(\"my favorite fruit is apple\", config=config)\n",
    "docs  # 검색 결과인 docs를 출력합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ffdad0be",
   "metadata": {},
   "source": [
    "이번에는 검색시 모든 검색 결과의 가중치가 **FAISS retriever 에 더 많이 부여** 되도록 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5d95922b",
   "metadata": {},
   "outputs": [],
   "source": [
    "config = {\"configurable\": {\"ensemble_weights\": [0, 1]}}\n",
    "\n",
    "# config 매개변수를 사용하여 검색 설정을 지정합니다.\n",
    "docs = ensemble_retriever.invoke(\"my favorite fruit is apple\", config=config)\n",
    "docs  # 검색 결과인 docs를 출력합니다."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
